<!DOCTYPE html>
<html>

<head>
    <title>Bootstrap5 实例</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="static/bootstrap.min.css" rel="stylesheet">
    <script src="static/bootstrap.bundle.min.js"></script>
    <script src="static/jquery.min.js"></script>
    <script src="static/three.min.js"></script>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }

        #three-container {
            width: 100%;
            height: 100%;
        }

        #toolbox {
            position: absolute;
            top: 10px;
            left: 10px;
            background-color: rgba(255, 255, 255, 0.1);
            /* 半透明背景 */
            padding: 10px;
            border-radius: 5px;
            z-index: 1000;
            /* 确保覆盖在Three.js渲染内容之上 */
        }
    </style>
</head>

<body>
    <div id="three-container"></div>
    <div id="toolbox">
        <div class="input-group mb-2">
            <span id="show-time" class="btn btn-info" onclick="hc.show_tools()">0</span>
            <span id="state-message" class="input-group-text"></span>
        </div>
        <div id="tools" style="display:none;">
            <div class="input-group mb-1">
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">主视图</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">后视图</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">左视图</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">右视图</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">俯视图</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">顺时针</button>
                <button type="button" class="form-control btn btn-primary" onclick="hc.view(this)">逆时针</button>
            </div>
            <div class="input-group mb-1">
                <span id="posColorList" class="badge bg-info" style="width: 35px;">0</span>
                <button type="button" class="btn btn-primary" onclick="hc.go_step(this)">-</button>
                <input class="form-control" type="range" id="myRange" onchange="hc.go_step(this)" value="0" min="0"
                    max="0" step="1">
                <button type="button" class="btn btn-primary" onclick="hc.go_step(this)">+</button>
                <button id="man-auto" type="button" class="btn btn-primary" onclick="hc.go_step(this)">》</button>
            </div>
            <textarea class="form-control" rows="3" id="sgftext" onchange="hc.getSGF(this)"></textarea>
        </div>
    </div>
    <script>
        var health_alert = false;
        function health() {
            const now = new Date();
            const minutes = now.getMinutes();
            if (minutes === 0 || minutes === 30) {
                if (health_alert) {
                    alert("游戏健康忠告：请注意休息，合理安排游戏时间。");
                    health_alert = false;
                }
            } else {
                health_alert = true;
            }
        }

        class ThreeBase { //创建网页的基本Three环境
            constructor(threeContainerId) {
                this.scene = this.create_scene("LightBlue"); //场景
                this.camera = this.create_camera([25, 25, 25]); //相机
                this.renderer = this.create_renderer(); //渲染器
                let threeContainer = document.getElementById(threeContainerId);
                threeContainer.appendChild(this.renderer.domElement);
                this.groupBase = new THREE.Group(); //场景用分组
                this.scene.add(this.groupBase);
                this.groupBase.add(new THREE.AxesHelper(5));
                this.setupClickListener(this.renderer.domElement, this); //渲染器canvas的监听事件
            }
            create_scene(color) {
                let scene = new THREE.Scene();
                scene.background = new THREE.Color(color);
                return scene;
            }
            create_camera(position = [10, 10, 10], origin = [0, 0, 0]) {
                let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
                camera.position.set(position[0], position[1], position[2]);
                camera.lookAt(new THREE.Vector3(origin[0], origin[1], origin[2]));
                return camera;
            }
            create_renderer() {
                let renderer = new THREE.WebGLRenderer({
                    antialias: true,
                    alpha: true
                });
                renderer.setSize(window.innerWidth, window.innerHeight);
                renderer.shadowMap.enabled = true;
                renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 可选：使用柔和阴影
                renderer.setClearAlpha(0.5);
                return renderer;
            }
            animate() { //动画
                const currentTime = performance.now();
                const elapsedTime = currentTime - this.startTime;
                if (currentTime % 1000 < 100) {
                    if (this.is1000ms) { //每秒执行一次
                        health();
                        $("#show-time").text(Number($("#show-time").text()) + 1);
                        if ($("#state-message").text() != "" && this.is5s < 5) {
                            this.is5s++;
                        } else if ($("#state-message").text() != "") {
                            $("#state-message").text("");
                            this.is5s = 0;
                        }
                    }
                    this.is1000ms = false;
                } else {
                    this.is1000ms = true;
                }
                if (this.isRotation) { //相机顺时针或者逆时针旋转
                    let clockValue = 0
                    if (this.antiClockwise == 1) {
                        clockValue = -0.002;
                    } else if (this.antiClockwise == 2) {
                        clockValue = 0.002;
                    }
                    let radius, theta, phi, x, y, z;
                    [radius, theta, phi] = this.cartesianToSpherical(this.camera.position.x, this.camera.position.y, this.camera.position.z);
                    [x, y, z] = this.sphericalToCartesian(radius, theta, phi + clockValue);
                    this.camera.position.set(x, y, z);
                    this.camera.lookAt(new THREE.Vector3(0, 0, 0));
                }
                this.renderer.render(this.scene, this.camera); //渲染器刷新
                this.animationFrameId = requestAnimationFrame(this.animate.bind(this)); //按屏幕刷新频率执行动画
            }
            animate_init() { //初始化动画环境，并启动
                this.startTime = null;
                this.animationFrameId = null;
                this.is1000ms = true;
                this.animate_start();
            }
            animate_start() { //动画启动功能
                if (this.animationFrameId == null) {
                    this.startTime = performance.now();
                    this.animate();
                }
            }
            animate_stop() { //动画停止功能
                if (this.animationFrameId !== null) {
                    cancelAnimationFrame(this.animationFrameId);
                    this.animationFrameId = null;
                }
            }
            setupClickListener(element, someVariable) {
                element.addEventListener('wheel', function (event) { //鼠标滚轮缩放画布
                    let delta = 0;
                    if (event.type === 'wheel') {
                        delta = -event.deltaY;
                    } else if (event.type === 'mousewheel') {
                        delta = event.wheelDelta;
                    }
                    let radius, theta, phi, x, y, z;
                    [radius, theta, phi] = someVariable.cartesianToSpherical(someVariable.camera.position.x, someVariable.camera.position.y, someVariable.camera.position.z);
                    [x, y, z] = someVariable.sphericalToCartesian(radius + delta / 100, theta, phi);
                    someVariable.camera.position.set(x, y, z);
                    someVariable.camera.lookAt(new THREE.Vector3(0, 0, 0));
                });
                element.addEventListener('mousedown', function (event) { //左键，右键按下初始化功能
                    if (event.button == 0) {
                        this.painting = true;
                        someVariable.left_listener(event.clientX, event.clientY);
                    } else if (event.button == 1) {
                        this.paintingM = true;
                        let x = someVariable.camera.position.x;
                        let y = someVariable.camera.position.y;
                        let z = someVariable.camera.position.z;
                        let [radius, theta, phi] = someVariable.cartesianToSpherical(x, z, y);
                        someVariable.basePosition = [event.clientX, event.clientY, radius, theta, phi];
                    }
                });
                element.addEventListener('mousemove', function (event) { //按下左键，右键移动事件
                    if (this.painting) {
                        console.log(event.button, event.clientX, event.clientY);
                    } else if (this.paintingM) {
                        someVariable.camera_rotate(event);
                    }
                });
                element.addEventListener('mouseup', function (event) { //鼠标左键，右键弹起事件
                    if (this.painting) {
                        this.painting = false;
                    } else if (this.paintingM) {
                        this.paintingM = false;
                    }
                });
            }
            cartesianToSpherical(x, y, z) { //笛卡尔坐标转球面坐标
                //r = sqrt(x^2 + y^2 + z^2), θ = arccos(z / r), φ = arctan2(y, x)
                let radius, theta, phi;
                radius = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2));
                theta = Math.acos(z / radius);
                phi = Math.atan2(y, x);
                return [radius, theta, phi];
            }
            sphericalToCartesian(radius, theta, phi) { //球面坐标转笛卡尔坐标
                //x=rsin(θ)cos(ϕ), y=rsin(θ)sin(ϕ), z=rcos(θ)
                let x = radius * Math.sin(theta) * Math.cos(phi);
                let y = radius * Math.sin(theta) * Math.sin(phi);
                let z = radius * Math.cos(theta);
                return [x, y, z];
            }
            camera_rotate(event) { //鼠标中键按住移动时，旋转相机坐标
                const mouseX = event.clientX;
                const mouseY = event.clientY;
                var radius = this.basePosition[2];
                var theta = this.minmax(mouseX - this.basePosition[0], -300, 300) / 300 * Math.PI / 2 + this.basePosition[3];
                var phi = this.minmax(this.basePosition[1] - mouseY, -200, 200) / 200 * Math.PI / 2 + this.basePosition[4];
                let [x, z, y] = this.sphericalToCartesian(radius, phi, theta);
                this.camera.position.set(x, y, z);
                this.camera.lookAt(new THREE.Vector3(0, 0, 0));
            }
            minmax(coordinate, min, max) {
                return Math.min(max, Math.max(min, coordinate));
            }
            left_listener(clientX, clientY) { //左键监听事件，在继承中被覆盖
                let mouse = new THREE.Vector2();
                mouse.x = (clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(clientY / window.innerHeight) * 2 + 1;
                let raycaster = new THREE.Raycaster();
                raycaster.setFromCamera(mouse, this.camera);
                let intersects = raycaster.intersectObjects(this.group1.children, true);
                if (intersects.length > 0) {
                    let intersect = intersects[0];
                    console.log(intersect.point.x, intersect.point.y)
                }
            }
        }

        class ThreeGeometry extends ThreeBase { //测试简单的VR功能
            constructor(threeContainerId) {
                super(threeContainerId)
                this.groupGeometry = new THREE.Group();
                this.scene.add(this.groupGeometry);
                this.main_geometry();
            }
            main_geometry() { }
            create_cube(position = [0, 0, 0], size = [1, 1, 1], color = "Gray") {
                /* BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
                width — X 轴上面的宽度，默认值为 1。
                height — Y 轴上面的高度，默认值为 1。
                depth — Z 轴上面的深度，默认值为 1。
                widthSegments — （可选）宽度的分段数，默认值是 1。
                heightSegments — （可选）高度的分段数，默认值是 1。
                depthSegments — （可选）深度的分段数，默认值是 1。 */
                const geometry = new THREE.BoxGeometry(size[0], size[1], size[2]);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const cube = new THREE.Mesh(geometry, material);
                cube.position.set(position[0], position[1], position[2]);
                return cube;
            }
            create_capsule(position = [0, 0, 0], radius = 1, length = 1, color = "Gray") {
                /* radius — 胶囊半径。可选的; 默认值为1。
                length — 中间区域的长度。可选的; 默认值为1。
                capSegments — 构造盖子的曲线部分的个数。可选的; 默认值为4。
                radialSegments — 覆盖胶囊圆周的分离的面的个数。可选的; 默认值为8。 */
                const geometry = new THREE.CapsuleGeometry(radius, length, 4, 8);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const capsule = new THREE.Mesh(geometry, material);
                capsule.position.set(position[0], position[1], position[2]);
                return capsule;
            }
            create_circle(position = [0, 0, 0], radius = 1, color = "Gray") {
                /* radius — 圆形的半径，默认值为1
                segments — 分段（三角面）的数量，最小值为3，默认值为32。
                thetaStart — 第一个分段的起始角度，默认为0。（three o'clock position）
                thetaLength — 圆形扇区的中心角，通常被称为“θ”（西塔）。默认值是2*Pi，这使其成为一个完整的圆。 */
                const geometry = new THREE.CircleGeometry(radius, 32);
                const material = new THREE.MeshBasicMaterial({ color: color });
                material.side = THREE.DoubleSide;
                const circle = new THREE.Mesh(geometry, material);
                circle.position.set(position[0], position[1], position[2]);
                return circle;
            }
            create_cone(position = [0, 0, 0], radius = 1, height = 1, color = "Gray") {
                /* radius — 圆锥底部的半径，默认值为1。
                height — 圆锥的高度，默认值为1。
                radialSegments — 圆锥侧面周围的分段数，默认为32。
                heightSegments — 圆锥侧面沿着其高度的分段数，默认值为1。
                openEnded — 一个Boolean值，指明该圆锥的底面是开放的还是封顶的。默认值为false，即其底面默认是封顶的。
                thetaStart — 第一个分段的起始角度，默认为0。（three o'clock position）
                thetaLength — 圆锥底面圆扇区的中心角，通常被称为“θ”（西塔）。默认值是2*Pi，这使其成为一个完整的圆锥。 */
                const geometry = new THREE.ConeGeometry(radius, height, 32);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const cone = new THREE.Mesh(geometry, material);
                cone.position.set(position[0], position[1], position[2]);
                return cone;
            }
            create_cylinder(position = [0, 0, 0], radiusTop = 1, radiusBottom = 1, height = 1, color = "Gray") {
                /* radiusTop — 圆柱的顶部半径，默认值是1。
                radiusBottom — 圆柱的底部半径，默认值是1。
                height — 圆柱的高度，默认值是1。
                radialSegments — 圆柱侧面周围的分段数，默认为32。
                heightSegments — 圆柱侧面沿着其高度的分段数，默认值为1。
                openEnded — 一个Boolean值，指明该圆锥的底面是开放的还是封顶的。默认值为false，即其底面默认是封顶的。
                thetaStart — 第一个分段的起始角度，默认为0。（three o'clock position）
                thetaLength — 圆柱底面圆扇区的中心角，通常被称为“θ”（西塔）。默认值是2*Pi，这使其成为一个完整的圆柱。 */
                const geometry = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 32);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const cylinder = new THREE.Mesh(geometry, material);
                cylinder.position.set(position[0], position[1], position[2]);
                return cylinder;
            }
            create_extrude(position = [0, 0, 0], color = "Gray") {
                /* shapes — 形状或者一个包含形状的数组。
                options — 一个包含有下列参数的对象：

                curveSegments — int，曲线上点的数量，默认值是12。
                steps — int，用于沿着挤出样条的深度细分的点的数量，默认值为1。
                depth — float，挤出的形状的深度，默认值为1。
                bevelEnabled — bool，对挤出的形状应用是否斜角，默认值为true。
                bevelThickness — float，设置原始形状上斜角的厚度。默认值为0.2。
                bevelSize — float。斜角与原始形状轮廓之间的延伸距离，默认值为bevelThickness-0.1。
                bevelOffset — float. Distance from the shape outline that the bevel starts. Default is 0.
                bevelSegments — int。斜角的分段层数，默认值为3。
                extrudePath — THREE.Curve对象。一条沿着被挤出形状的三维样条线。Bevels not supported for path extrusion.
                UVGenerator — Object。提供了UV生成器函数的对象。
                该对象将一个二维形状挤出为一个三维几何体。

                当使用这个几何体创建Mesh的时候，如果你希望分别对它的表面和它挤出的侧面使用单独的材质，你可以使用一个材质数组。 第一个材质将用于其表面；第二个材质则将用于其挤压出的侧面。 */
                const length = 1, width = 2;

                const shape = new THREE.Shape();
                shape.moveTo(0, 0);
                shape.lineTo(0, width);
                shape.lineTo(length, width);
                shape.lineTo(length, 0);
                shape.lineTo(0, 0);

                const extrudeSettings = {
                    steps: 2,
                    depth: 3,
                    bevelEnabled: true,
                    bevelThickness: 1.5,
                    bevelSize: 0.5,
                    bevelOffset: 0,
                    bevelSegments: 1
                };

                const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const mesh = new THREE.Mesh(geometry, material);
                mesh.position.set(position[0], position[1], position[2]);
                return mesh;
            }
            create_icosahedron(position = [0, 0, 0], radius = 1, color = "Gray") {
                /* radius — 二十面体的半径，默认为1。
                detail — 默认值为0。将这个值设为一个大于0的数将会为它增加一些顶点，使其不再是一个二十面体。当这个值大于1的时候，实际上它将变成一个球体。 */
                const geometry = new THREE.IcosahedronGeometry(radius);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const mesh = new THREE.Mesh(geometry, material);
                mesh.position.set(position[0], position[1], position[2]);
                return mesh;
            }
            create_lathe(position = [0, 0, 0], color = "Gray") {
                /* points — 一个Vector2对象数组。每个点的X坐标必须大于0。 Default is an array with (0,-0.5), (0.5,0) and (0,0.5) which creates a simple diamond shape.
                segments — 要生成的车削几何体圆周分段的数量，默认值是12。
                phiStart — 以弧度表示的起始角度，默认值为0。
                phiLength — 车削部分的弧度（0-2PI）范围，2PI将是一个完全闭合的、完整的车削几何体，小于2PI是部分的车削。默认值是2PI。

                基于参数创建一个LatheGeometry。 */
                const points = [];
                for (let i = 0; i < 3; i++) {
                    points.push(new THREE.Vector2(Math.sin(i * 0.2) * 2 + 1, (i - 1) * 2));
                }
                const geometry = new THREE.LatheGeometry(points);
                const material = new THREE.MeshBasicMaterial({ color: color });
                material.side = THREE.DoubleSide;
                const lathe = new THREE.Mesh(geometry, material);
                lathe.position.set(position[0], position[1], position[2]);
                return lathe;
            }
            create_octahedron(position = [0, 0, 0], radius = 1, color = "Gray") {
                /* radius — 八面体的半径，默认值为1。
                detail — 默认值为0，将这个值设为一个大于0的数将会为它增加一些顶点，使其不再是一个八面体。 */
                const geometry = new THREE.OctahedronGeometry(radius);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const mesh = new THREE.Mesh(geometry, material);
                mesh.position.set(position[0], position[1], position[2]);
                return mesh;
            }
            create_plane(position = [0, 0, 0], width = 1, height = 1, color = "Gray") {
                /* width — 平面沿着 X 轴的宽度。默认值是 1。
                height — 平面沿着 Y 轴的高度。默认值是 1。
                widthSegments — （可选）平面的宽度分段数，默认值是 1。
                heightSegments — （可选）平面的高度分段数，默认值是 1。 */
                const geometry = new THREE.PlaneGeometry(width, height);
                const material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide });
                const plane = new THREE.Mesh(geometry, material);
                plane.position.set(position[0], position[1], position[2]);
                return plane;
            }
            create_dodecahedron(position = [0, 0, 0], radius = 1, color = "Gray") {
                /* radius — 十二面体的半径，默认值为1。
                detail — 默认值为0。将这个值设为一个大于0的数将会为它增加一些顶点，使其不再是一个十二面体。 */
                const geometry = new THREE.DodecahedronGeometry(radius);
                const material = new THREE.MeshBasicMaterial({ color: color });
                const dodecahedron = new THREE.Mesh(geometry, material);
                dodecahedron.position.set(position[0], position[1], position[2]);
                return dodecahedron;
            }
            create_edges(position = [0, 0, 0], radius = 1, color = "Gray") {
                /* geometry — 任何一个几何体对象。
                thresholdAngle — 仅当相邻面的法线之间的角度（单位为角度）超过这个值时，才会渲染边缘。默认值为1。 */
                const geometry = new THREE.DodecahedronGeometry(radius);
                const edges = new THREE.EdgesGeometry(geometry);
                const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: color }));
                line.position.set(position[0], position[1], position[2]);
                return line;
            }
        }

        class ThreeVR extends ThreeGeometry { //测试简单的VR功能
            constructor(threeContainerId) {
                super(threeContainerId)
                this.groupVR = new THREE.Group();
                this.scene.add(this.groupVR);
                this.mainVR();
            }
            mainVR() {
                this.groupGeometry.add(this.create_cube());
                this.groupGeometry.add(this.create_capsule([3, 3, 3]));
                this.groupGeometry.add(this.create_circle([-3, 3, 3]));
                this.groupGeometry.add(this.create_cone([3, -3, 3]));
                this.groupGeometry.add(this.create_cylinder([3, 3, -3], 0.5, 1));
                this.groupGeometry.add(this.create_extrude([-3, -3, 3]));
                this.groupGeometry.add(this.create_icosahedron([-3, 3, -3]));
                this.groupGeometry.add(this.create_lathe([3, -3, -3]));
                this.groupGeometry.add(this.create_octahedron([6, 6, 6]));
                this.groupGeometry.add(this.create_plane([-6, 6, 6]));
                this.groupGeometry.add(this.create_dodecahedron([6, -6, 6]));
                this.groupGeometry.add(this.create_edges([6, 6, -6]));
            }
        }

        class htmlClick extends ThreeVR {
            constructor(threeContainerId) {
                super(threeContainerId);
                this.animate_init();
            }
            view(tt) {
                let radius, theta, phi, x, y, z;
                radius = Math.sqrt(Math.pow(this.camera.position.x, 2) + Math.pow(this.camera.position.y, 2) + Math.pow(this.camera.position.z, 2));
                this.camera.up.set(0, 0, 1);
                this.antiClockwise = 0;
                this.isRotation = false;
                switch ($(tt).text()) {
                    case "主视图":
                        phi = Math.PI / 6;
                        theta = Math.PI * 1.5;
                        break;
                    case "后视图":
                        phi = Math.PI / 6;
                        theta = Math.PI * 0.5;
                        break;
                    case "左视图":
                        phi = Math.PI / 6;
                        theta = Math.PI;
                        break;
                    case "右视图":
                        phi = Math.PI / 6;
                        theta = 0;
                        break;
                    case "俯视图":
                        this.camera.up.set(0, 1, 0);
                        phi = 0;
                        theta = 0;
                        break;
                    case "逆时针":
                        this.antiClockwise = 1;
                        this.isRotation = true;
                        return;
                    case "顺时针":
                        this.antiClockwise = 2;
                        this.isRotation = true;
                        return;
                    default:
                        console.log($(tt).val());
                        return;
                }
                [x, y, z] = this.sphericalToCartesian(radius, phi, theta);
                this.camera.position.set(x, y, z);
                this.camera.lookAt(new THREE.Vector3(0, 0, 0));
            }
            show_tools() {
                $("#tools").toggle();
                $("#state-message").text("健康忠告：请注意休息，合理安排时间。");
            }
        }
        const hc = new htmlClick("three-container");

        window.addEventListener('resize', () => {
            hc.camera.aspect = window.innerWidth / window.innerHeight;
            hc.camera.updateProjectionMatrix();
            hc.renderer.setSize(window.innerWidth, window.innerHeight);
        });

    </script>
</body>

</html>